HEX
Server: Apache/2.4.68 (Debian)
System: Linux as-cs-widget-demo-us-central1 6.1.0-44-cloud-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.164-1 (2026-03-09) x86_64
User: root (0)
PHP: 8.2.32
Disabled: NONE
Upload Files
File: /var/www/kevin-demo/wp-content/plugins/wpforms-sendinblue/src/Provider/Settings/FormBuilder.php
<?php

namespace WPFormsSendinblue\Provider\Settings;

use Exception;
use WPFormsSendinblue\Plugin;
use WPFormsSendinblue\Api\Connection;
use WPForms\Providers\Provider\Settings\FormBuilder as FormBuilderAbstract;

/**
 * Class FormBuilder handles functionality inside the form builder.
 *
 * @since 1.0.0
 */
class FormBuilder extends FormBuilderAbstract {

	/**
	 * Lists of account fetched from Sendinblue.
	 *
	 * @since 1.1.0
	 *
	 * @var null|array
	 */
	private $sendinblue_lists = null;

	/**
	 * Register all hooks (actions and filters).
	 *
	 * @since 1.0.0
	 */
	protected function init_hooks() {

		parent::init_hooks();

		// AJAX-event names.
		static $ajax_events = [
			'ajax_account_save',
			'ajax_account_template_get',
			'ajax_connections_get',
			'ajax_accounts_get',
			'ajax_action_data_get',
		];

		// Register callbacks for AJAX events.
		array_walk(
			$ajax_events,
			static function ( $ajax_event, $key, $instance ) {

				add_filter(
					"wpforms_providers_settings_builder_{$ajax_event}_{$instance->core->slug}",
					[ $instance, $ajax_event ]
				);
			},
			$this
		);

		// Register callbacks for hooks.
		add_filter( 'wpforms_save_form_args', [ $this, 'save_form' ], 11, 2 );
	}

	/**
	 * Pre-process provider data before saving it in form_data when editing form.
	 *
	 * @since 1.0.0
	 *
	 * @param array $form Form array, usable with wp_update_post.
	 * @param array $data Data retrieved from $_POST and processed.
	 *
	 * @return array
	 */
	public function save_form( $form, $data ) {

		// Get a filtered (or modified by another addon) form content.
		$form_data = json_decode( stripslashes( $form['post_content'] ), true );

		// Provider exists.
		if ( ! empty( $form_data['providers'][ Plugin::SLUG ] ) ) {
			$modified_post_content = $this->modify_form_data( $form_data );

			if ( ! empty( $modified_post_content ) ) {
				$form['post_content'] = wpforms_encode( $modified_post_content );

				return $form;
			}
		}

		/*
		 * This part works when modification is locked or current filter was called on NOT Providers panel.
		 * Then we need to restore provider connections from the previous form content.
		 */

		// Get a "previous" form content (current content is still not saved).
		$prev_form = ! empty( $data['id'] ) ? wpforms()->obj( 'form' )->get( $data['id'], [ 'content_only' => true ] ) : [];

		if ( ! empty( $prev_form['providers'][ Plugin::SLUG ] ) ) {
			$provider = $prev_form['providers'][ Plugin::SLUG ];

			if ( ! isset( $form_data['providers'] ) ) {
				$form_data = array_merge( $form_data, [ 'providers' => [] ] );
			}

			$form_data['providers'] = array_merge( (array) $form_data['providers'], [ Plugin::SLUG => $provider ] );
			$form['post_content']   = wpforms_encode( $form_data );
		}

		return $form;
	}

	/**
	 * Prepare modifications for the form content, if it's not locked.
	 *
	 * @since 1.0.0
	 *
	 * @param array $form_data Form content.
	 *
	 * @return array
	 */
	protected function modify_form_data( $form_data ) {

		/**
		 * Connection is locked.
		 * Why? User clicked the "Save" button when one of AJAX requests
		 * for retrieving data from API was in progress or failed.
		 */
		if (
			isset( $form_data['providers'][ Plugin::SLUG ]['__lock__'] ) &&
			absint( $form_data['providers'][ Plugin::SLUG ]['__lock__'] ) === 1
		) {
			return [];
		}

		// Modify content as we need, done by reference.
		foreach ( $form_data['providers'][ Plugin::SLUG ] as $connection_id => $connection ) {
			if ( '__lock__' === $connection_id ) {
				unset( $form_data['providers'][ Plugin::SLUG ]['__lock__'] );
				continue;
			}

			$form_data['providers'][ Plugin::SLUG ][ $connection_id ] = $this->sanitize_connection( $connection );
		}

		return $form_data;
	}

	/**
	 * Sanitize connection.
	 *
	 * @since 1.0.0
	 *
	 * @param array $connection_data Connection data.
	 *
	 * @return array
	 */
	private function sanitize_connection( $connection_data ) {

		if ( ! empty( $connection_data['fields_required'] ) ) {
			$connection_data['fields_required'] = $this->sanitize_required_fields( $connection_data['fields_required'] );
		}

		if ( ! empty( $connection_data['fields_meta'] ) ) {
			$connection_data['fields_meta'] = $this->sanitize_custom_fields( $connection_data['fields_meta'] );
		}

		if ( ! empty( $connection_data['event_name'] ) ) {
			$connection_data['event_name'] = wpforms_sanitize_text_deeply( $connection_data['fields_meta'] );
		}

		return $connection_data;
	}

	/**
	 * Sanitize required fields.
	 *
	 * @since 1.0.0
	 *
	 * @param array $fields List of fields.
	 *
	 * @return array
	 */
	private function sanitize_required_fields( $fields ) {

		foreach ( $fields as $key => &$field ) {
			foreach ( $field as $name => &$value ) {
				if ( wpforms_is_empty_string( $value ) ) {
					unset( $fields[ $key ] );
					continue;
				}

				$value = $name === 'field_id' || $name === 'list_id' ? absint( $value ) : sanitize_text_field( $value );
			}
		}

		return $fields;
	}

	/**
	 * Sanitize custom fields.
	 *
	 * @since 1.0.0
	 *
	 * @param array $fields List of fields.
	 *
	 * @return array
	 */
	private function sanitize_custom_fields( $fields ) {

		if ( empty( $fields ) ) {
			return [];
		}

		foreach ( $fields as $key => &$field ) {
			// Remove field if name or field_id is empty.
			if ( wpforms_is_empty_string( $field['name'] ) || wpforms_is_empty_string( $field['field_id'] ) ) {
				unset( $fields[ $key ] );
				continue;
			}

			$field['name']     = wpforms_sanitize_text_deeply( $field['name'] );
			$field['field_id'] = sanitize_text_field( $field['field_id'] );
		}

		return $fields;
	}

	/**
	 * Save the data for a new account and validate it.
	 *
	 * @since 1.0.0
	 */
	public function ajax_account_save() {

		$data         = wp_unslash( wp_parse_args( $_POST['data'] ) ); // phpcs:ignore
		$api_key      = ! empty( $data['api_key'] ) ? sanitize_text_field( $data['api_key'] ) : '';
		$account_name = ! empty( $data['account_name'] ) ? sanitize_text_field( $data['account_name'] ) : '';

		try {
			wpforms_sendinblue()
				->get( 'account' )
				->add( $api_key, $account_name );

			wp_send_json_success();
		} catch ( Exception $e ) {
			wp_send_json_error(
				[
					'error_msg' => esc_html__( 'Invalid Brevo API credentials. Please check your information and try again.', 'wpforms-sendinblue' ),
				]
			);
		}
	}

	/**
	 * Content for Add New Account modal.
	 *
	 * @since 1.0.0
	 *
	 * @return array
	 */
	public function ajax_account_template_get() {

		return [
			'title'      => esc_html__( 'New Brevo Account', 'wpforms-sendinblue' ),
			'content'    => wpforms_sendinblue()->get( 'account' )->get_form(),
			'connection' => '',
			'type'       => 'blue',
		];
	}

	/**
	 * Get the list of all saved connections.
	 *
	 * @since 1.0.0
	 *
	 * @return array
	 */
	public function ajax_connections_get() {

		$connections = [
			'connections'  => ! empty( $this->get_connections_data() ) ? array_reverse( $this->get_connections_data(), true ) : [],
			'conditionals' => [],
		];

		// Get conditional logic for each connection ID.
		foreach ( $connections['connections'] as $connection ) {
			if ( empty( $connection['id'] ) ) {
				continue;
			}

			// This will either return an empty placeholder or complete set of rules, as a DOM.
			$connections['conditionals'][ $connection['id'] ] = wpforms_conditional_logic()
				->builder_block(
					[
						'form'       => $this->form_data,
						'type'       => 'panel',
						'parent'     => 'providers',
						'panel'      => Plugin::SLUG,
						'subsection' => $connection['id'],
						// TODO: Since WPForms 1.9.2, this property will be redundant and can be removed once addon requirements are updated.
						'reference'  => esc_html__( 'Marketing provider connection', 'wpforms-sendinblue' ),
					],
					false
				);
		}

		$accounts = $this->ajax_accounts_get();

		return array_merge( $connections, $accounts );
	}

	/**
	 * Get the list of all accounts.
	 *
	 * @since 1.0.0
	 *
	 * @return array May return an empty sub-array.
	 */
	public function ajax_accounts_get() {

		return [
			'accounts' => $this->get_accounts(),
			'actions'  => $this->get_actions(),
		];
	}

	/**
	 * Retrieve saved provider accounts data.
	 *
	 * @since 1.0.0
	 *
	 * @return array
	 */
	private function get_accounts() {

		return wpforms_sendinblue()->get( 'account' )->get_accounts();
	}

	/**
	 * Retrieve saved provider connections data.
	 *
	 * @since 1.0.0
	 *
	 * @return array
	 */
	public function get_connections_data() {

		return isset( $this->form_data['providers'][ Plugin::SLUG ] )
			? $this->form_data['providers'][ Plugin::SLUG ]
			: [];
	}

	/**
	 * Get all lists for account.
	 *
	 * @since 1.0.0
	 * @since 1.1.0 Remove the usage of transient to cache the lists.
	 *                  Instead, use `$this->sendinblue_lists` to hold the lists.
	 *
	 * @param string $account_id Account ID.
	 *
	 * @return array
	 */
	private function get_lists( $account_id ) {

		if ( ! is_null( $this->sendinblue_lists ) ) {
			return $this->sendinblue_lists;
		}

		$connection = wpforms_sendinblue()
			->get( 'account' )
			->get_connection( $account_id );

		if ( ! $connection ) {
			$this->sendinblue_lists = [];

			return [];
		}

		$lists = $this->get_all_lists( $connection );

		if ( empty( $lists ) ) {
			$this->sendinblue_lists = [];

			return [];
		}

		$lists = wp_list_pluck( $lists, 'name', 'id' );

		// Delete an auxiliary list.
		$default_key = array_search( 'identified_contacts', $lists, true );

		if ( $default_key ) {
			unset( $lists[ $default_key ] );
		}

		$this->sendinblue_lists = $lists;

		return $lists;
	}

	/**
	 * Get all lists.
	 *
	 * @since 1.0.0
	 *
	 * @param Connection $connection Get all lists.
	 *
	 * @return array
	 */
	private function get_all_lists( $connection ) {

		$lists    = [];
		$offset   = 0;
		$per_page = 50; // Is max per page value.

		try {
			do {
				$body = $connection
					->get_lists( $per_page, $offset )
					->get_body();

				if ( empty( $body['lists'] ) ) {
					return [];
				}

				$lists = array_merge( $lists, $body['lists'] );
				$count = ! empty( $body['count'] ) ? $body['count'] : 0;

				$offset += $per_page;
			} while ( $offset < $count );
		} catch ( Exception $e ) {
			// Redefine the list as an empty array because on the previous
			// do/while block only the 2nd or 3rd iteration could generate an exception.
			// We are not sure what will be in that $lists (that is partially filled),
			// thus we are defining it here as an empty array again.
			$lists = [];
		}

		return $lists;
	}

	/**
	 * Get all opt-in templates for account.
	 *
	 * @since 1.1.0
	 *
	 * @param string $account_id Account ID.
	 *
	 * @return array
	 */
	private function get_opt_in_templates( $account_id ) {

		$connection = wpforms_sendinblue()
			->get( 'account' )
			->get_connection( $account_id );

		if ( ! $connection ) {
			return [];
		}

		$templates = $this->get_all_opt_in_templates( $connection );

		if ( empty( $templates ) ) {
			return [];
		}

		$templates = array_filter( $templates, [ $this, 'is_opt_in_template' ] );

		return wp_list_pluck( $templates, 'name', 'id' );
	}

	/**
	 * Is it an opt-in template?
	 *
	 * @since 1.1.0
	 *
	 * @param array $template Template data.
	 *
	 * @return bool
	 *
	 * @see   https://help.sendinblue.com/hc/en-us/articles/360019540880
	 */
	private function is_opt_in_template( $template ) {

		if ( empty( $template['tag'] ) || empty( $template['htmlContent'] ) ) {
			return false;
		}

		/**
		 * For opt-in templates required the `optin` tag.
		 *
		 * @see https://help.sendinblue.com/hc/en-us/articles/360019540880
		 */
		return $template['tag'] === 'optin' && strpos( $template['htmlContent'], '{{ doubleoptin }}' ) !== false;
	}

	/**
	 * Get all templates.
	 *
	 * @since 1.1.0
	 *
	 * @param Connection $connection Get all lists.
	 *
	 * @return array
	 */
	private function get_all_opt_in_templates( $connection ) {

		$templates = [];
		$offset    = 0;
		$per_page  = 1000; // Is max per page value.

		try {
			do {
				$body = $connection
					->get_active_templates( $per_page, $offset )
					->get_body();

				if ( empty( $body['templates'] ) ) {
					return [];
				}

				$templates = array_merge( $templates, $body['templates'] );
				$count     = ! empty( $body['count'] ) ? $body['count'] : 0;

				$offset += $per_page;
			} while ( $offset < $count );
		} catch ( Exception $e ) {
			// Redefine templates as an empty array because on the previous
			// do/while block only the 2nd or 3rd iteration could generate an exception.
			// We are not sure what will be in that $templates (that is partially filled),
			// thus we are defining it here as an empty array again.
			$templates = [];
		}

		return $templates;
	}

	/**
	 * Get list of attributes for account.
	 *
	 * @since 1.0.0
	 *
	 * @param string $account_id Account ID.
	 *
	 * @return array
	 */
	private function get_attributes( $account_id ) {

		$connection = wpforms_sendinblue()
			->get( 'account' )
			->get_connection( $account_id );

		if ( ! $connection ) {
			return [];
		}

		$attributes = $connection->get_attributes();
		$body       = $attributes->get_body();

		if ( empty( $body['attributes'] ) ) {
			return [];
		}

		$attributes = [];

		foreach ( $body['attributes'] as $attribute ) {
			$attribute = (array) $attribute;
			// Service has a global category as auxiliary types.
			if ( $attribute['category'] === 'global' ) {
				continue;
			}

			$attributes[ $attribute['name'] ] = $attribute['name'];
		}

		return $attributes;
	}

	/**
	 * Retrieve Sendinblue action data.
	 *
	 * @since 1.0.0
	 *
	 * @return array
	 */
	public function ajax_action_data_get() {

		if ( empty( $_POST['account_id'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
			return [];
		}

		$account_id = sanitize_text_field( wp_unslash( $_POST['account_id'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing

		if ( ! wpforms_sendinblue()->get( 'account' )->account_exists( $account_id ) ) {
			return [];
		}

		return [
			'subscribe'   => $this->get_subscribe_fields( $account_id ),
			'unsubscribe' => $this->get_unsubscribe_fields( $account_id ),
			'delete'      => $this->get_delete_fields(),
			'track_event' => $this->get_track_event_fields(),
		];
	}

	/**
	 * Get fields for the subscribe action.
	 *
	 * @since 1.0.0
	 *
	 * @param string $account_id Account ID.
	 *
	 * @return array
	 */
	private function get_subscribe_fields( $account_id ) {

		return [
			'required' => [
				'email'        => [
					'label'    => esc_html__( 'Email', 'wpforms-sendinblue' ),
					'type'     => 'email_select',
					'required' => true,
				],
				'new_email'    => [
					'label' => esc_html__( 'New Email', 'wpforms-sendinblue' ),
					'type'  => 'email_select',
				],
				'list'         => [
					'label'    => esc_html__( 'List', 'wpforms-sendinblue' ),
					'type'     => 'select',
					'required' => true,
					'options'  => $this->get_lists( $account_id ),
				],
				'opt_in'       => [
					'label' => esc_html__( 'Enable double opt-in', 'wpforms-sendinblue' ),
					'type'  => 'toggle',
					'desc'  => esc_html__( 'Send contacts an opt-in confirmation email when they subscribe to audience.', 'wpforms-sendinblue' ),
				],
				'template_id'  => [
					'label'    => esc_html__( 'Template', 'wpforms-sendinblue' ),
					'type'     => 'select',
					'required' => true,
					'options'  => $this->get_opt_in_templates( $account_id ),
				],
				'redirect_url' => [
					'label'       => esc_html__( 'Redirect URL', 'wpforms-sendinblue' ),
					'type'        => 'url',
					'required'    => true,
					'placeholder' => 'https://',
					'desc'        => esc_html__( 'URL of the web page that the user will be redirected to after clicking on the double opt-in button (button with the {{ doubleoptin }} tag).', 'wpforms-sendinblue' ),
				],
			],
			'optional' => $this->get_attributes( $account_id ),
		];
	}

	/**
	 * Get fields for the unsubscribe action.
	 *
	 * @since 1.0.0
	 *
	 * @param string $account_id Account ID.
	 *
	 * @return array
	 */
	private function get_unsubscribe_fields( $account_id ) {

		return [
			'required' => [
				'email' => [
					'label'    => esc_html__( 'Email', 'wpforms-sendinblue' ),
					'type'     => 'email_select',
					'required' => true,
				],
				'list'  => [
					'label'    => esc_html__( 'List', 'wpforms-sendinblue' ),
					'type'     => 'select',
					'required' => true,
					'options'  => $this->get_lists( $account_id ),
				],
			],
			'optional' => [],
		];
	}

	/**
	 * Get fields for the delete action.
	 *
	 * @since 1.0.0
	 *
	 * @return array
	 */
	private function get_delete_fields() {

		return [
			'required' => [
				'email' => [
					'label'    => esc_html__( 'Email', 'wpforms-sendinblue' ),
					'type'     => 'email_select',
					'required' => true,
				],
			],
			'optional' => [],
		];
	}

	/**
	 * Get fields for the track event action.
	 *
	 * @since 1.0.0
	 *
	 * @return array
	 */
	private function get_track_event_fields() {

		return [
			'required' => [
				'email'      => [
					'label'    => esc_html__( 'Email', 'wpforms-sendinblue' ),
					'type'     => 'email_select',
					'required' => true,
				],
				'event_name' => [
					'label'    => esc_html__( 'Event name', 'wpforms-sendinblue' ),
					'type'     => 'text',
					'required' => true,
				],
			],
			'optional' => [],
		];
	}

	/**
	 * Get list of actions.
	 *
	 * @since 1.0.0
	 *
	 * @return array
	 */
	private function get_actions() {

		return [
			'subscribe'   => esc_html__( 'Subscribe', 'wpforms-sendinblue' ),
			'unsubscribe' => esc_html__( 'Unsubscribe', 'wpforms-sendinblue' ),
			'delete'      => esc_html__( 'Delete', 'wpforms-sendinblue' ),
			'track_event' => esc_html__( 'Track Event', 'wpforms-sendinblue' ),
		];
	}

	/**
	 * Use this method to register own templates for form builder.
	 * Make sure, that you have `tmpl-` in template name in `<script id="tmpl-*">`.
	 *
	 * @since 1.0.0
	 */
	public function builder_custom_templates() {

		?>
		<!-- Single Sendinblue connection. -->
		<script type="text/html" id="tmpl-wpforms-<?php echo esc_attr( Plugin::SLUG ); ?>-builder-content-connection">
			<?php echo wpforms_sendinblue()->get( 'template' )->get_builder_template( 'connection' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
		</script>

		<!-- Single Sendinblue connection block: REQUIRED EMAIL SELECT FIELD. -->
		<script type="text/html" id="tmpl-wpforms-<?php echo esc_attr( Plugin::SLUG ); ?>-builder-content-connection-required-email-select-field">
			<?php echo wpforms_sendinblue()->get( 'template' )->get_builder_template( 'required-email-select-field' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
		</script>

		<!-- Single Sendinblue connection block: REQUIRED SELECT FIELD. -->
		<script type="text/html" id="tmpl-wpforms-<?php echo esc_attr( Plugin::SLUG ); ?>-builder-content-connection-required-select-field">
			<?php echo wpforms_sendinblue()->get( 'template' )->get_builder_template( 'required-select-field' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
		</script>

		<!-- Single Sendinblue connection block: REQUIRED TEXT FIELD. -->
		<script type="text/html" id="tmpl-wpforms-<?php echo esc_attr( Plugin::SLUG ); ?>-builder-content-connection-required-text-field">
			<?php echo wpforms_sendinblue()->get( 'template' )->get_builder_template( 'required-text-field' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
		</script>

		<!-- Single Sendinblue connection block: REQUIRED CHECKBOX FIELD. -->
		<script type="text/html" id="tmpl-wpforms-<?php echo esc_attr( Plugin::SLUG ); ?>-builder-content-connection-required-toggle-field">
			<?php echo wpforms_sendinblue()->get( 'template' )->get_builder_template( 'required-toggle-field' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
		</script>

		<!-- Single Sendinblue connection block: REQUIRED CHECKBOX FIELD. -->
		<script type="text/html" id="tmpl-wpforms-<?php echo esc_attr( Plugin::SLUG ); ?>-builder-content-connection-required-url-field">
			<?php echo wpforms_sendinblue()->get( 'template' )->get_builder_template( 'required-url-field' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
		</script>

		<!-- Single Sendinblue connection block: ERROR. -->
		<script type="text/html" id="tmpl-wpforms-<?php echo esc_attr( Plugin::SLUG ); ?>-builder-content-connection-error">
			<?php echo wpforms_sendinblue()->get( 'template' )->get_builder_template( 'error' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
		</script>
		<?php
	}

	/**
	 * Enqueue JavaScript and CSS files.
	 *
	 * @since 1.0.0
	 */
	public function enqueue_assets() {

		parent::enqueue_assets();

		$min = wpforms_get_min_suffix();

		wp_enqueue_script(
			'wpforms-sendinblue-admin-builder',
			WPFORMS_SENDINBLUE_URL . "assets/js/sendinblue-builder{$min}.js",
			[ 'wpforms-admin-builder-providers' ],
			WPFORMS_SENDINBLUE_VERSION,
			true
		);

		wp_localize_script(
			'wpforms-sendinblue-admin-builder',
			'wpformsSendinblueBuilderVars',
			[
				'l10n' => [
					'provider_placeholder' => esc_html__( '--- Select Brevo Field ---', 'wpforms-sendinblue' ),
					// The `nameFieldFormats` key is deprecated since 1.5.0.
					'nameFieldFormats'     => [
						'full'   => esc_html__( 'Full', 'wpforms-sendinblue' ),
						'first'  => esc_html__( 'First', 'wpforms-sendinblue' ),
						'middle' => esc_html__( 'Middle', 'wpforms-sendinblue' ),
						'last'   => esc_html__( 'Last', 'wpforms-sendinblue' ),
					],
				],
			]
		);
	}
}